package com.aratana;

import java.awt.Container;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;

import org.apache.log4j.Logger;

import com.aratana.converter.Converter;

public final class MVCUnit {

	// default converts
	private static final HashMap<Class<?>, HashMap<Class<?>, Converter<?, ?>>> converters = new HashMap<Class<?>, HashMap<Class<?>, Converter<?, ?>>>();
	static {
		initDefaultConverters();
	}

	private final HashMap<Class<?>, HashMap<Class<?>, Converter<?, ?>>> myConverters = new HashMap<Class<?>, HashMap<Class<?>, Converter<?, ?>>>();

	private final Logger logger;

	private final Model model;
	private final View view;
	private final Controller controller;

	private final String name;

	MVCUnit(String name, Container container, Object asModel, Object asController) {
		this.name = name;
		this.model = new Model(this);
		this.controller = new Controller(this);
		this.view = new View(container, this);
		if (asModel != null) {
			this.model.addAsModel(asModel);
		}
		if (asController != null) {
			this.controller.addAsController(asController);
		}
		logger = Logger.getLogger("aratana." + getName());
		logger.info("MVCUnit '" + name + "' initialized.");
	}

	public View getView() {
		return view;
	}

	public Model getModel() {
		return model;
	}

	public Controller getController() {
		return controller;
	}

	public String getName() {
		return name;
	}

	void handleException(Exception e) {
		e.printStackTrace();
	}

	static void handleExceptionDefault(Exception e) {
		e.printStackTrace();
	}

	public static final MVCUnit create(String name, Container container) {
		return new MVCUnit(name, container, null, null);
	}

	public static final MVCUnit create(String name, Container container, Object asModel, Object asController) {
		return new MVCUnit(name, container, asModel, asController);
	}

	public final <From, To> void registerConverter(final Converter<From, To> converter) {
		try {
			HashMap<Class<?>, Converter<?, ?>> map = myConverters.get(converter.getFromClass());
			if (map == null) {
				myConverters.put(converter.getFromClass(), map = new HashMap<Class<?>, Converter<?, ?>>());
			}
			map.put(converter.getToClass(), converter);
		} catch (final Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public <To, From> To convert(final From from,final  Class<To> to) throws Exception {
		if (from == null) {
			return null;
		}
		
		Class<?> fromClass = toWrapper(from.getClass());
		Class<?> toClass = toWrapper(to);
		
		if (fromClass == toClass) {
			return (To) from;
		}
		HashMap<Class<?>, Converter<?, ?>> hashMap = myConverters.get(fromClass);
		if (hashMap == null) {
			hashMap = converters.get(from.getClass());
		}
		if (hashMap != null) {
			final Converter<From, To> converter = (Converter<From, To>) hashMap.get(toClass);
			if (converter != null) {
				return converter.convert(from);
			}
		}

		throw new RuntimeException("No converter found for " + from.getClass().getSimpleName() + " to " + toClass.getSimpleName());
	}
	
	private static final HashMap<Class<?>,Class<?>> primitives = new HashMap<Class<?>, Class<?>>();
	
	static{
		primitives.put(boolean.class, Boolean.class);
		primitives.put(byte.class, Byte.class);
		primitives.put(char.class, Character.class);
		primitives.put(short.class, Short.class);
		primitives.put(float.class, Float.class);
		primitives.put(int.class, Integer.class);
		primitives.put(long.class, Long.class);
		primitives.put(double.class, Double.class);
	}
	
	private static final Class<?> toWrapper(Class<?> primitiveClass){
		Class<?> result = primitives.get(primitiveClass);
		return result == null ? primitiveClass : result;
	}
	
	private static final void initDefaultConverters() {
		
		// Long - Integer
		registerDefaultConverter(new Converter<Long, Integer>() {

			@Override
			public Integer convert(final Long from) throws Exception {
				return from.intValue();
			}
		});
		
		// Integer - Long
		registerDefaultConverter(new Converter<Integer, Long>() {

			@Override
			public Long convert(final Integer from) throws Exception {
				return from.longValue();
			}
		});

		// Long - String
		registerDefaultConverter(new Converter<Long, String>() {

			@Override
			public String convert(final Long from) throws Exception {
				return from == null ? null : String.valueOf(from);
			}
		});

		// Date - String
		registerDefaultConverter(new Converter<Date, String>() {

			private final SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");

			@Override
			public String convert(final Date from) throws Exception {
				return format.format(from);
			}
		});

		// String - Date
		registerDefaultConverter(new Converter<String, Date>() {

			private final SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");

			@Override
			public Date convert(final String from) throws Exception {
				return format.parse(from);
			}
		});

		// Date - Long
		registerDefaultConverter(new Converter<Date, Long>() {

			@Override
			public Long convert(final Date from) throws Exception {
				return from.getTime();
			}
		});

		// String - Boolean
		registerDefaultConverter(new Converter<String, Boolean>() {

			private final Set<String> truth = new HashSet<String>();

			{
				truth.add("S");
				truth.add("Y");
				truth.add("SIM");
				truth.add("YES");
			}

			@Override
			public Boolean convert(final String from) throws Exception {
				return from != null && truth.contains(from.toUpperCase());
			}
		});

		// Timestamp - Date
		registerDefaultConverter(new Converter<Timestamp, Date>() {

			@Override
			public Date convert(final Timestamp from) throws Exception {
				return new Date(from.getTime());
			}
		});
	}

	public static final <From, To> void registerDefaultConverter(final Converter<From, To> converter) {
		try {
			HashMap<Class<?>, Converter<?, ?>> map = converters.get(converter.getFromClass());
			if (map == null) {
				converters.put(converter.getFromClass(), map = new HashMap<Class<?>, Converter<?, ?>>());
			}
			map.put(converter.getToClass(), converter);
		} catch (final Exception e) {
			handleExceptionDefault(e);
		}
	}

	@SuppressWarnings("unchecked")
	public static final <T> T getPreparedModelOriginalObject(T preparedModelObject) {
		if (preparedModelObject instanceof PreparedModel<?>) {
			return ((PreparedModel<T>) preparedModelObject).getModelObject();
		} else {
			return preparedModelObject;
		}

	}

	private static final HashMap<Class<?>, Class<?>> preparedClasses = new HashMap<Class<?>, Class<?>>();

	@SuppressWarnings("unchecked")
	public static final <T> T prepareModel(T modelObject) {
		try {
			Class<T> preparedClass = (Class<T>) preparedClasses.get(modelObject.getClass());
			if (preparedClass == null) {
				ClassPool classPool = ClassPool.getDefault();
				CtClass base = classPool.get(modelObject.getClass().getName());
				CtClass prepared = classPool.makeClass(modelObject.getClass().getName() + "$EnhancedByAratana", base);
				prepared.addInterface(classPool.get(PreparedModel.class.getName()));

				// units field
				prepared.addField(CtField.make(" private final java.util.WeakHashMap mvcUnits = new java.util.WeakHashMap();", prepared));
				// modelObject
				prepared.addField(CtField.make(" private final " + modelObject.getClass().getName() + " modelObject;", prepared));

				CtConstructor constructor = new CtConstructor(new CtClass[] { base }, prepared);
				constructor.setBody("{super();this.modelObject = $1;}");

				prepared.addConstructor(constructor);

				// implement addMVCUnit
				prepared.addMethod(CtNewMethod.make(/**/
				"public void addToUnit(com.aratana.MVCUnit unit){ " + /**/
				"	mvcUnits.put($1,null); " + /**/
				" }"/**/
				, prepared));

				// implement getModelObject()
				prepared.addMethod(CtNewMethod.make(/**/
				"public " + modelObject.getClass().getName() + " getModelObject(){ " + /**/
				"	return this.modelObject; " + /**/
				" }"/**/
				, prepared));

				prepared.addMethod(CtNewMethod.make(/**/
				" private void updateViews(String name){" + /**/
				"	java.util.Iterator iterator = mvcUnits.keySet().iterator();" + /**/
				"	while (iterator.hasNext()) {" + /**/
				"		((com.aratana.MVCUnit)iterator.next()).getView().updateFromModel(name);" + /**/
				"	}" + /**/
				" }"/**/
				, prepared));

				for (CtMethod baseMethod : base.getMethods()) {
					if (Modifier.isPublic(baseMethod.getModifiers()) && !Modifier.isStatic(baseMethod.getModifiers()) && !Modifier.isFinal(baseMethod.getModifiers()) && !Modifier.isNative(baseMethod.getModifiers())) {
						if (baseMethod.getName().startsWith("set")) {
							CtMethod preparedMethod = CtNewMethod.copy(baseMethod, prepared, null);
							if (preparedMethod.getParameterTypes()[0] == prepared) {
								preparedMethod = CtNewMethod.make("public void " + preparedMethod.getName() + "(" + base.getName() + " arg){}", prepared);
							}
							preparedMethod.setBody("{ this.modelObject." + baseMethod.getName() + "($$);" + /**/
							" updateViews(\"" + (Character.toLowerCase(baseMethod.getName().charAt(3)) + baseMethod.getName().substring(4)) + "\");}");
							prepared.addMethod(preparedMethod);
						} else {
							CtMethod preparedMethod = CtNewMethod.copy(baseMethod, prepared, null);
							if (preparedMethod.getReturnType() == prepared) {
								preparedMethod = CtNewMethod.make("public " + base.getName() + " " + preparedMethod.getName() + "(){return null;}", prepared);
							}
							preparedMethod.setBody("{ " + (baseMethod.getReturnType() != CtClass.voidType ? "return" : "") + " this.modelObject." + baseMethod.getName() + "($$);}");
							prepared.addMethod(preparedMethod);
						}
					}
				}

				preparedClasses.put(modelObject.getClass(), preparedClass = prepared.toClass());
			}

			return (T) preparedClass.getConstructors()[0].newInstance(modelObject);
		} catch (Exception e) {
			handleExceptionDefault(e);
			return null;
		}
	}
}
